Scala Cookbook翻译 Chapter 1.Strings 第三部分

1.8 提取一个匹配模式的字符串的部分

  • 问题:你想要提取出一个字符串中的一个或多个部分,以匹配你指定的正则表达式模式。

    1.8.1 解决方案

  • 定义要提取的正则表达式模式,将圆括号放在他们周围,所以可以把它们作为“正则表达式组”,首先定义所需的模式:

    val pattern = "([0-9]+) ([A-Za-z]+)".r
    
  • 然后从目标字符串中提取正则表达式组

    val pattern(count, fruit) = "100 Bananas"
    
  • 此代码从给定的字符串中提取出数字字段和字母字段作为两个独立的变量,count和fruit。

    scala> val pattern = "([0-9]+) ([A-Za-z]+)".r
    pattern: scala.util.matching.Regex = ([0-9]+) ([A-Za-z]+)
    
    scala> val pattern(count, fruit) = "100 Bananas"
    count: String = 100
    fruit: String = Bananas
    

1.8.2 讨论

  • 这个语法可能有点不寻常,因为看上去是两次定义模式作为一个变量,但这种语法在现实例子中更加方便和可读。
  • 想象一下你正在编写一个像谷歌一样的搜索引擎,而且你想让人们使用各种各样的短语搜索电影。要非常方便的,你可以让他们键入任何这些短语获得一个movies,near,Boulder,Colorado的列表。

    "movies near 80301"
    "movies 80301"
    "80301 movies"
    "movie: 80301"
    "movies: 80301"
    "movies near boulder, co"
    "movies near boulder, colorado"
    
  • 一种可以允许所有短语都被使用到的方法就是定义一系列正则表达式模式来进行匹配。只要定义你的表达式,然后尝试匹配任何你所允许的所有可能的表达式。举例,你只允许下面两个简单的模式。

    // match "movies 80301"
    val MoviesZipRE = "movies (\\d{5})".r
    
    // match "movies near boulder, co"
    val MoviesNearCityStateRE = "movies near ([a-z]+), ([a-z]{2})".r
    
  • 一旦你定义了你想允许的模式,无论用户键入什么都可以使用匹配表达式进行匹配。下例中,当需要匹配时可以调用一个虚构的方法,叫做getSearchResults。

    textUserTyped match {
        case MoviesZipRE(zip) => getSearchResults(zip)
        case MoviesNearCityStateRE(city, state) => getSearchResults(city, state)
        case _ => println("did not match a regex")
    }
    
  • 正如你看到的,这个语法使你匹配表达式的可读性很强。对于你要匹配的两个模式,你可以调用getSearchResults方法的重载方法,把zip字段传递给第一个case,把city和state字段传递给第二个case。这个例子中展示的两个正则表达式会匹配如下的字符串:

    "movies 80301"
    "movies 99676"
    "movies near boulder, co"
    "movies near talkeetna, ak"
    
  • 值得注意的是,正则表达式必须匹配用户的输入。根据现在的正则表达式模式,如下的字符串将会匹配失败,因为每行的末尾有一个空格。

    "movies 80301 "
    "movies near boulder, co "
    
  • 可以通过去除输入的首尾空格来解决这个特别的问题,或者使用一个更加复杂的正则表达式,想要在真实世界中做任何事。

  • 可以想象,你可以在很多不同的情况下使用相同的匹配模式,包括匹配日期和时间格式,街道地址,人的名字,等等很多其他情况。

1.9 访问字符串中的字符

  • 问题:在字符串的特定位置获得一个字符。

    1.9.1 解决方案

  • 可以使用Java的charAt方法

    scala> "hello".charAt(0)
    res0: Char = h
    
  • 然后首选方式是使用Scala的数组表示法:

    scala> "hello"(0)
    res1: Char = h
    
    scala> "hello"(1)
    res2: Char = e
    

1.9.2 讨论

  • 当循环字符串中的字符时,通常会使用map方法和foreach方法。但是某些情况下这些方法可能不起作用,此时可以把字符串当做一个Array数组,然后可以通过数组表示法获取每个字符。
  • Scala的数组表示法和Java的不同因为在Scala中它真的是一个方法调用,有一些很好的语法。如果你这样写代码将非常方便并且很易读。

    scala> "hello"(1)
    res0: Char = e
    
  • 但在幕后,Scala将你的代码转成如下:

    scala> "hello".apply(1)
    res1: Char = e
    
  • 这个语法详解在第6.8章“不使用new关键词创建对象实例”

1.10 在String类中自定义方法

  • 问题:不是创建一个单独的字符串实用程序库,像StringUtilities类一样,你想要添加你自己的行为到String类中,所以你可以如下编写代码:

    "HAL".increment
    
  • 而不是这个:

    StringUtilities.increment("HAL")
    

1.10.1 解决方案

  • Scala2.10里,定义一个隐式类,然后再类里定义方法执行想要的行为。
  • 可以在REPL中看到,首先定义隐式类和方法:

    scala> implicit class StringImprovements(s: String) {
    | def increment = s.map(c => (c + 1).toChar)
    | }
    defined class StringImprovements
    
  • 然后在任何字符串上调用你的方法:

    scala> val result = "HAL".increment
    result: String = IBM
    
  • 实际代码中,这只有一点复杂而已。根据SIP-13 - Implicit classes,“隐式类必须在允许方法定义(不在顶层)的范围内定义”,意味着隐式类必须在类、对象或包对象中定义。

1.10.2 把隐类放在一个对象上

  • 一种满足这种情况的方法就是把隐类放在一个对象里。举例,你可以把StringImprovements隐类放入一个对象里,如StringUtils对象:

    package com.alvinalexander.utils
    
    object StringUtils {
    
        implicit class StringImprovements(val s: String) {
            def increment = s.map(c => (c + 1).toChar)
        }
    }
    
  • 然后就可以在添加适当的import语句后在任何地方使用increment方法:

    package foo.bar
    
    import com.alvinalexander.utils.StringUtils._
    
    object Main extends App {
    
        println("HAL".increment)
    }
    

1.10.3 把隐类放在一个包对象上

  • 另一个满足要求的方法是把隐类放在一个包对象上。用这种方法,把下面的代码放到适当的目录下的一个命名为package.scala的文件中,如果使用SBT,应该把文件放到你项目src/main/scala/com/alvinalexander的目录下,文件包括以下内容:
  • 注SBT:SBT是Simple Build Tool的简称,如果使用过Maven,那么可以简单将SBT看做是Scala世界的Maven,虽然二者各有优劣,但完成的工作基本是类似的。可以使用SBT构建Scala应用。

    package com.alvinalexander
    
    package object utils {
    
        implicit class StringImprovements(val s: String) {
            def increment = s.map(c => (c + 1).toChar)
        }
    }
    
  • 当需要在其他代码中使用increment方法时,从前面的示例中使用一个稍微不用的import语句:

    package foo.bar
    
    import com.alvinalexander.utils._
    
    object MainDriver extends App {
        println("HAL".increment)
    }
    
  • 详情见6.7章节,“把普通代码放在包对象中”。

1.10.4 使用Scala 2.10之前版本

  • 如果因为某些原因需要使用Scala2.10之前的版本,则需要采用一个稍微不同的方法。在这种情况下,在一个一般Scala类中定义一个increment方法:

    class StringImprovements(val s: String) {
        def increment = s.map(c => (c + 1).toChar)
    }
    
  • 然后定义一个处理隐式转换的另一个方法:

    implicit def stringToString(s: String) = new StringImprovements(s)
    
  • stringToString方法中的字符串参数本质上链接String类到StringImprovements类,现在就可以和之前例子一样使用increment方法了。

    "HAL".increment
    
  • REPL中演示如下:

    scala> class StringImprovements(val s: String) {
        | def increment = s.map(c => (c + 1).toChar)
        | }
    defined class StringImprovements
    
    scala> implicit def stringToString(s: String) = new StringImprovements(s)
    stringToString: (s: String)StringImprovements
    
    scala> "HAL".increment
    res0: String = IBM
    

1.10.5 讨论

  • 正如之前所见,在Scala中,可以通过写隐式转换在闭类里添加新的功能,当你需要时把他们添加到作用域。这种方法的主要好处就是不需要继承已存在的类来添加新的功能。例如,没有必要创建一个继承String的新类MyString,然后在代码中使用MyString代替String。相反,你可以定义你想要的行为,然后将该行为添加到当前作用域的所有的String对象中,当你添加导入语句时。
  • 值得注意的是你可以在隐类中定义许多你需要的方法。下面的代码显示了increment和decrement方法,以及一个hideAll方法返回一个所有字符都被“*”字符替换的字符串。

    implicit class StringImprovements(val s: String) {
        def increment = s.map(c => (c + 1).toChar)
        def decrement = s.map(c => (c − 1).toChar)
        def hideAll = s.replaceAll(".", "*")
    }
    
  • 注意,除了类名前的implicit关键词,StringImprovements类和他的方法和往常一个写。

  • 通过导入语句简单的将代码带到范围里,你可以使用这些方法,REPL中显示如下:

    scala> "HAL".increment
    res0: String = IBM
    
  • 有一个关于它如何工作的简单描述:
    • 编译器看到字符串“HAL”
    • 编译器看到你正在尝试调用一个String类里的increment方法
    • 因为编译器无法再String类里找到这个方法,它开始开始查寻找周围的隐式转换方法,并且接受一个字符串参数
    • 这个将编译器指向StringImprovements类,在那里它会找到increment方法
  • 这是工作过程的简单化,但它给了隐式转换如何工作的总体思路。

1.10.6 注释你的方法的返回类型

  • 建议应该注释掉隐式方法定义的返回类型,如果你的运行环境编译器无法找到你的隐式方法,或者你只想显示的声明你的方法,添加你的方法定义的返回类型。
  • 在increment,decrement和hideAll方法中,返回字符串的类型是显示的:

    implicit class StringImprovements(val s: String) {
        // being explicit that each method returns a String
        def increment: String = s.map(c => (c + 1).toChar)
        def decrement: String = s.map(c => (c − 1).toChar)
        def hideAll: String = s.replaceAll(".", "*")
    }
    

1.10.7 返回其他类型

  • 尽管到目前所显示的所有方法都返回一个字符串,你可以从你的方法里返回你需要的任何类型。下面的类演示了几种不同的字符串转换方法类型。

    implicit class StringImprovements(val s: String) {
        def increment = s.map(c => (c + 1).toChar)
        def decrement = s.map(c => (c − 1).toChar)
        def hideAll: String = s.replaceAll(".", "*")
        def plusOne = s.toInt + 1
        def asBoolean = s match {
            case "0" | "zero" | "" | " " => false
            case _ => true
        }
    }
    
  • 有了这些新方法,可以执行Int和Boolean的转换,除了之前显示的字符串转换:

    scala> "4".plusOne
    res0: Int = 5
    
    scala> "0".asBoolean
    res1: Boolean = false
    
    scala> "1".asBoolean
    res2: Boolean = true
    

* 值得注意的是这些方法已经简化,以保持它们的简短和可读性。在实际代码中,你可能会添加一些错误检查。